#include "xmppcore/xmppxmlparser.h"

namespace yoohoo_xmppcore {

const QChar* XmppXmlParser::FLAG_START_ELEMENT = new QChar('<');
const QChar* XmppXmlParser::FLAG_END_TAG = new QChar('>');
const QString XmppXmlParser::FLAG_END_ELEMENT = "</";
const QString XmppXmlParser::FLAG_EMPTY_ELEMENT_END_TAG = "/>";
const QString XmppXmlParser::FLAG_START_CDATA = "<![cdata[";
const QString XmppXmlParser::FLAG_END_CDATA = "]]>";
const QString XmppXmlParser::FLAG_START_XML_DOC_DECL = "<?xml";
const QString XmppXmlParser::FLAG_END_XML_DOC_DECL = "?>";
const QChar* XmppXmlParser::FLAG_START_ATTRIBUTE_VALUE = new QChar('\'');
const QString XmppXmlParser::STREAM_TAG_NAME = "stream:stream";

XmppXmlParser::XmppXmlParser() {
	_debug = false;
}

void XmppXmlParser::setDebug(bool debug) {
	_debug = debug;
}

bool XmppXmlParser::debug() {
	return _debug;
}

QList<QString> XmppXmlParser::parse(QString& dataReceived) {
	QList<QString> elements;
	QList<QString> tagStack;
	ParsingStatus status = FindNextToken;
	int pos = 0;
	int tagStartIndex = 0;

	while (true) {
CheckStatus:
		if (dataReceived.size() == 0 || pos == (dataReceived.size() - 1)) {
			if (_debug)
				qDebug("Need wait data. Data[%s]", qPrintable(dataReceived));

			return elements;
		} else if (status == FindNextToken) {
			if (!findNextToken(dataReceived, pos)) {
				goto CheckStatus;
			}

			if (_debug)
				qDebug("XmppXmlParser::parse: Try to find next token. Current position[%d], Data[%s]", pos, qPrintable(dataReceived));

			if (isXmlDocDeclStart(dataReceived, pos)) {
				status = StartXmlDocDecl;
			} else if (isCDataStart(dataReceived, pos)) {
				status = StartCData;
			} else if (/*tagStack.size() > 0 && */isTagEnd(dataReceived, pos)) {
				status = EndTag;
			} else {
				status = StartTag;
			}
		} else if (status == StartCData) {
			do {
				pos++;
				if (dataReceived.at(pos) == FLAG_END_CDATA.at(2)) { // find a '>'
					if (dataReceived.at(pos - 1) == FLAG_END_CDATA.at(1) &&
							dataReceived.at(pos - 2) == FLAG_END_CDATA.at(0)) { // find ']]>'
						status = FindNextToken;
						goto CheckStatus;
					}
				}
			} while (pos < (dataReceived.size() - 1));
		} else if (status == EndTag) {
			pos++;
			if (isCloseStreamStart(dataReceived, pos)) {
				if (_debug)
					qDebug("Found a close stream");

				// remove all chars before close stream. substract 1 because we need return to previous '<' char
				pos -= 1;
				dataReceived.remove(0, pos);

				pos = (STREAM_TAG_NAME.size() + 2); // plus 2 because '/' char and '>' char in '</stream:stream>'
				elementFound(dataReceived, pos, elements);
				status = FindNextToken;
			} else if (tagStack.size() == 0) { // no according start tag?
				status = Error;
			} else {
				QString tagName = tagStack.takeAt(0);
				// becase we want to check whether a '>' follows tag name, so we plus 1 char.
				if ((pos + tagName.size() + 1) >= dataReceived.size()) {
					pos = dataReceived.size() - 1;
					goto CheckStatus;
				}

				for (int i = 0; i < tagName.size(); i++) {
					pos++;
					if (dataReceived.at(pos) != tagName.at(i)) {
						status = Error;
						goto CheckStatus;
					}
				}

				pos++;
				if (dataReceived[pos] != *FLAG_END_TAG) {
					status = Error;
					goto CheckStatus;
				}

				if (_debug)
					qDebug("Found a end tag[%s]", qPrintable(tagName));

				if (tagStack.size() == 0) {
					elementFound(dataReceived, pos, elements);
				}

				status = FindNextToken;
			}
		} else if (status == Error) {
			if (pos != 0) { // remove error data before current position
				dataReceived.remove(0, pos + 1);

				if (_debug)
					qDebug("Remove some error data. Remainder data[%s]", qPrintable(dataReceived));
			}

			pos = 0;
			status = FindNextToken;
		} else if (status == StartXmlDocDecl) {
			// try to find xml doc declaration end
			do {
				pos++;
				if (dataReceived.at(pos) == FLAG_END_XML_DOC_DECL.at(1)) { // find a '>'
					if (dataReceived.at(pos - 1) == FLAG_END_XML_DOC_DECL.at(0)) { // find '?>', xml doc declaration is well-formed
						status = EndXmlDocDecl;
					} else { // not '?>', xml doc declaration isn't well-formed
						status = Error;
					}

					goto CheckStatus;
				}
			} while (pos < (dataReceived.size() - 1));
		} else if (status == EndXmlDocDecl) {
			elementFound(dataReceived, pos, elements);
			status = FindNextToken;
		} else if (status == StartTag) {
			tagStartIndex = pos + 1;
			// Try to find tag name end
			do {
				pos++;
				if (dataReceived.at(pos).isSpace()) {
					status = EndTagName;
					goto CheckStatus;
				}

				if (dataReceived.at(pos) == *FLAG_END_TAG) {
					if (dataReceived.at(pos - 1) == FLAG_EMPTY_ELEMENT_END_TAG.at(0)) {
						pos--; // it's a '/>'(empty element tag), return to position of '/'
					}

					status = EndTagName;
					goto CheckStatus;
				}
			} while (pos < (dataReceived.size() - 1));
		} else if (status == EndTagName) {
			// found a tag name
			QString tagName = dataReceived.mid(tagStartIndex, pos - tagStartIndex);
			
			if (_debug)
				qDebug("XmppXmlParser::parse: found a tag name[%s]", qPrintable(tagName));

			if (tagName == STREAM_TAG_NAME) {
				status = OpenStream;
				goto CheckStatus;
			}

			// remember tag name
			tagStack.push_front(tagName);
			pos--; // return to last char of end of tag name
			do {
				pos++;
				if (dataReceived.at(pos) == *FLAG_END_TAG) {
					if (dataReceived.at(pos - 1) == FLAG_EMPTY_ELEMENT_END_TAG.at(0)) {
						tagStack.pop_front();
						// found a doc only includes an empty element
						if (tagStack.size() == 0)
							elementFound(dataReceived, pos, elements);
						status = FindNextToken;
					} else {
						status = FindNextToken;
					}
					goto CheckStatus;
				} else if (dataReceived.at(pos) == *FLAG_START_ELEMENT) {
					pos--; // remove error data but reserve '<'
					status = Error;
					goto CheckStatus;
				}
			} while (pos < (dataReceived.size() - 1));
		} else if (status == OpenStream) {
			do {
				pos++;
				if (dataReceived.at(pos) == *FLAG_END_TAG) {
					elementFound(dataReceived, pos, elements);
					status = FindNextToken;
					goto CheckStatus;
				}
			} while (pos < (dataReceived.size() - 1));
		}
	}
}

bool XmppXmlParser::isCloseStreamStart(QString& dataReceived, int& pos) {
	int streamTagNameSize = STREAM_TAG_NAME.size();

	if ((pos + streamTagNameSize + 1) > (dataReceived.size() - 1)) // plus 1 for last char of end tag('>')
		return false;

	for (int i = 0; i < streamTagNameSize; i++) {
		// current char is '/' of end tag, so need plug 1 to access tag name
		if (dataReceived.at(pos + i + 1) != STREAM_TAG_NAME.at(i))
			return false;
	}

	return dataReceived.at(pos + streamTagNameSize + 1) == *FLAG_END_TAG;
}

bool XmppXmlParser::isTagEnd(QString& dataReceived, int& pos) {
	if ((pos + FLAG_END_ELEMENT.size()) > (dataReceived.size() - 1))
		return false;

	if (dataReceived.at(pos + 1) != FLAG_END_ELEMENT.at(1))
		return false;

	return true;
}

bool XmppXmlParser::findNextToken(QString& dataReceived, int& pos) {
	bool foundToken = false;

	int start = pos;
	while (pos < (dataReceived.size() - 1)) {
		if (dataReceived.at(pos) == *FLAG_START_ELEMENT) {
			foundToken = true;
			break;
		}

		pos++;
	}

	if (start == 0) { // before finding new node, we need remove redanduncy chars before '<'
		dataReceived.remove(0, pos);
		pos = 0;
	}

	return foundToken;
}

void XmppXmlParser::elementFound(QString& dataReceived, int& pos, QList<QString>& elements) {
	QString element = dataReceived.left(pos + 1);

	if (_debug)
		qDebug("XmppXmlParser::parse: found a message[%s]", qPrintable(element));

	dataReceived.remove(0, pos + 1);

	if (_debug)
		qDebug("Remainder data[%s]", qPrintable(dataReceived));

	elements.append(element);

	// remove redundancy chars before next element
	while (dataReceived.size() > 0 && dataReceived.at(0) != *FLAG_START_ELEMENT) {
		dataReceived.remove(0, 1);
	}

	pos = 0;
}

bool XmppXmlParser::isXmlDocDeclEnd(QString& dataReceived, int& pos) {
	return dataReceived.at(pos -1) == FLAG_END_XML_DOC_DECL.at(0) &&
			dataReceived.at(pos) == FLAG_END_XML_DOC_DECL.at(1);
}

bool XmppXmlParser::isCDataStart(QString& dataReceived, int& pos) {
	int cDataStartFlagSize = FLAG_END_CDATA.size();

	if ((pos + cDataStartFlagSize) > (dataReceived.size() - 1))
			return false;

	for (int i = 0; i < cDataStartFlagSize; i++) {
		if (dataReceived.at(pos + i) != FLAG_START_CDATA.at(i))
			return false;
	}

	pos += cDataStartFlagSize;
	return true;
}

bool XmppXmlParser::isXmlDocDeclStart(QString& dataReceived, int& pos) {
	int xmlDocDeclStartFlagSize = FLAG_START_XML_DOC_DECL.size();
	if ((pos + xmlDocDeclStartFlagSize) > (dataReceived.size() - 1))
		return false;

	for (int i = 0; i < xmlDocDeclStartFlagSize; i++) {
		if (dataReceived.at(pos + i) != FLAG_START_XML_DOC_DECL.at(i))
			return false;
	}

	pos += xmlDocDeclStartFlagSize;
	return true;
}

}
